#define debug
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using FederatedScaleOutDatabases.DomainModel;
using FederatedScaleOutDatabases.Interfaces;
using FederatedScaleOutDatabases.Utilities.LINQ;

namespace FederatedScaleOutDatabases.Strategies
{
    public class EmployeeFederationResolutionStrategy<T> : IFederationResolutionStrategy<T>
    {
        private readonly List<FederatedDatabase> _federatedDatabases;
        private Int32 _callCounter = 0;

        public ReadOnlyDictionary<FederatedDatabase, Dictionary<MemberInfo, MemberParams>> Mappings { get; set; }
        private readonly Dictionary<FederatedDatabase, Dictionary<MemberInfo, MemberParams>> _internalDictionary;
        public EmployeeFederationResolutionStrategy(List<FederatedDatabase> federatedDatabases)
        {
            _federatedDatabases = federatedDatabases;
            _internalDictionary = GetMappings();
            Mappings = new ReadOnlyDictionary<FederatedDatabase, Dictionary<MemberInfo, MemberParams>>(_internalDictionary);
        }

        public IEnumerable<FederatedDatabase> GetFederatedDatabase(T entity)
        {
            if (typeof(T) == typeof(Employee))
            {
                var employee = entity as Employee;
                var databasesToUse = new List<FederatedDatabase>();
                foreach (var mappingInfo in Mappings)
                {
                    bool entityMatchesMapping = false;
                    var mappingDetails = mappingInfo.Value;
                    foreach (var mapping in mappingDetails)
                    {
                        var propInfo = employee.GetType().GetProperty(mapping.Key.Name);
                        dynamic instanceValue = propInfo.GetValue(employee);
                        dynamic mappingValue = mapping.Value.ValueToCompare;

                        if (mapping.Value.Operator == ExpressionType.Equal)
                            entityMatchesMapping = instanceValue == mappingValue;
                        else if (mapping.Value.Operator == ExpressionType.NotEqual)
                            entityMatchesMapping = instanceValue != mappingValue;
                        else if (mapping.Value.Operator == ExpressionType.LessThan)
                            entityMatchesMapping = (instanceValue < mappingValue);
                        else if (mapping.Value.Operator == ExpressionType.LessThanOrEqual)
                            entityMatchesMapping = (instanceValue <= mappingValue);
                        else if (mapping.Value.Operator == ExpressionType.GreaterThan)
                            entityMatchesMapping = (instanceValue > mappingValue);
                        else if (mapping.Value.Operator == ExpressionType.GreaterThanOrEqual)
                            entityMatchesMapping = (instanceValue >= mappingValue);
                        else
                            entityMatchesMapping = false;
                        if (!entityMatchesMapping)
                            break;
                    }
                    if (entityMatchesMapping)
                        databasesToUse.Add(mappingInfo.Key);
                }

                return databasesToUse;
            }
            else
            {
                var databaseName = _callCounter % 2 == 0 ? "FedDb1" : "FedDb2";
                _callCounter++;
                return _federatedDatabases.Where(d => d.DatabaseName == databaseName);
            }
        }

        public IEnumerable<FederatedDatabase> GetFederatedDatabases(Expression<Func<T, bool>> predicateForQuery)
        {

            var databasesToUse = new List<FederatedDatabase>();
            foreach (var mappingInfo in Mappings)
            {
                var expressionComparer = new ExpressionEquivalenceComparer<T>(mappingInfo.Value);
                expressionComparer.Visit(predicateForQuery);
                if (expressionComparer.Result)
                    databasesToUse.Add(mappingInfo.Key);
            }

            if (databasesToUse.Count == 0)
                databasesToUse.AddRange(Mappings.Keys);

#if debug
            var currentBackColor = Console.BackgroundColor;
            var currentForeColor = Console.ForegroundColor;
            Console.BackgroundColor = ConsoleColor.Red;
            Console.ForegroundColor = ConsoleColor.White;
            Console.WriteLine("Accessing databases {0} for the query {1}", string.Join(",", databasesToUse.Select(d => d.DatabaseName)), predicateForQuery.ToString());
            Console.BackgroundColor = currentBackColor;
            Console.ForegroundColor = currentForeColor;
#endif

            return databasesToUse;
        }

        private Dictionary<FederatedDatabase, Dictionary<MemberInfo, MemberParams>> GetMappings()
        {
            var databaseMappings = new Dictionary<FederatedDatabase, Dictionary<MemberInfo, MemberParams>>();

            #region Mapping for FedDb1

            var fedDatabase1 = _federatedDatabases.Single(d => d.DatabaseName == "FedDb1");

            var mappings = new Dictionary<MemberInfo, MemberParams>();
            var mapping = GetMappingFor(emp => emp.City, ExpressionType.Equal, "Bangalore");

            mappings.Add(mapping.Item1, mapping.Item2);

            mapping = GetMappingFor(emp => emp.Designation, ExpressionType.Equal, ".NET Architect");

            mappings.Add(mapping.Item1, mapping.Item2);

            mapping = GetMappingFor(emp => emp.DeptId, ExpressionType.GreaterThan, 100);

            mappings.Add(mapping.Item1, mapping.Item2);

            databaseMappings.Add(fedDatabase1, mappings);

            #endregion

            #region Mapping for FedDb2

            var fedDatabase2 = _federatedDatabases.Single(d => d.DatabaseName == "FedDb2");

            mappings = new Dictionary<MemberInfo, MemberParams>();
            mapping = GetMappingFor(emp => emp.City, ExpressionType.Equal, "Delhi");

            mappings.Add(mapping.Item1, mapping.Item2);

            mapping = GetMappingFor(emp => emp.Designation, ExpressionType.Equal, "Solution Architect");

            mappings.Add(mapping.Item1, mapping.Item2);

            mapping = GetMappingFor(emp => emp.DeptId, ExpressionType.LessThanOrEqual, 100);

            mappings.Add(mapping.Item1, mapping.Item2);

            databaseMappings.Add(fedDatabase2, mappings);
            #endregion

            return databaseMappings;
        }

        private static Tuple<MemberInfo, MemberParams> GetMappingFor<TValue>(Expression<Func<Employee, TValue>> expression, ExpressionType operation, TValue valueToCompare)
        {
            return new Tuple<MemberInfo, MemberParams>(((MemberExpression)expression.Body).Member
                , new MemberParams()
                {
                    ValueToCompare = valueToCompare,
                    Operator = operation
                });
        }


    }
}
